// Copyright (C) Microsoft Corporation. All rights reserved.

using System;
using System.CodeDom.Compiler;
using System.Collections;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Resources;
using System.Security.Permissions;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

[assembly: CLSCompliant(true)]
[assembly: SecurityPermission(SecurityAction.RequestMinimum)]
namespace Microsoft.Build.Extras.FX1_1
{
    /// <summary>
    /// Implements the "GenerateResource" task, which invokes resgen.exe
    /// to compile .resx files into .resource files.
    /// </summary>
    public class GenerateResource : ToolTask
    {
        // Constants used when deleting the temp subdirectory for resgen.
        // Maximum number of delete attempts.
        protected const int MaxDeleteAttempts = 10;
        // Time between delete attempts in milliseconds.
        protected const int DeleteDelay = 100;

        public GenerateResource()
        {
            // Create a resource manager.
            TaskResources = new ResourceManager("Microsoft.Build.Extras.FX1_1.Strings", Assembly.GetExecutingAssembly());
        }

        #region Fields

        // FxCop wants me to create a property and keep the field private. However, any property I make is accessible 
        // by MSBuild via the task and I want this field hidden so I'm choosing to keep it protected.
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")]
        // Path to temp subdirectory where resgen.exe and any references will be copied to.
        protected string tempResGenPath;

        // This is where we store the list of files to be compiled into resources.
        private ITaskItem[] sources;

        // The list of resource file(s) generated by the task
        private ITaskItem[] outputResources;

        // List of paths to references.
        private ITaskItem[] references;

        #endregion  // fields

        #region ToolTask Members

        protected override string ToolName
        {
            get
            {
                return "Resgen.exe";
            }
        }

        /// <summary>
        /// GenerateFullPathToTool() returns a path to the Resgen.exe underneath the newly created
        /// temp subdirectory. 
        /// </summary>
        /// <returns>String containing a path to ResGen.exe.</returns>
        protected override string GenerateFullPathToTool()
        {
            return Path.Combine(Path.GetFullPath(tempResGenPath), ToolName);
        }

        /// <summary>
        /// OriginalPathToTool() returns a path to Resgen.exe underneath SDK\bin.
        /// </summary>
        /// <returns>String containing a path to Resgen.exe in the .NET SDK 1.1 directory.</returns>
        private string OriginalPathToTool()
        {
            string resgenPath = ToolLocationHelper.GetPathToDotNetFrameworkSdkFile(ToolName, TargetDotNetFrameworkVersion.Version11);

            if (String.IsNullOrEmpty(resgenPath))
            {
                Log.LogErrorWithCodeFromResources("PlatformSDKFileNotFound", ToolName, ToolLocationHelper.GetDotNetFrameworkVersionFolderPrefix(TargetDotNetFrameworkVersion.Version11),
                    ToolLocationHelper.GetDotNetFrameworkSdkInstallKeyValue(TargetDotNetFrameworkVersion.Version11),
                    ToolLocationHelper.GetDotNetFrameworkRootRegistryKey(TargetDotNetFrameworkVersion.Version11));
            }

            return resgenPath;
        }

        #endregion

        #region Properties

        /// <summary>
        /// The names of the items to be converted. The extension must be one of the
        /// following: .txt, .resx or .resources.
        /// </summary>
        [Required]
        public ITaskItem[] Sources
        {
            set { sources = value; }
            get { return sources; }
        }

        /// <summary>
        /// The name(s) of the resource file to create (as input) and that were created (as output). 
        /// </summary>
        /// <remarks>
        /// If the user does not provide any input, the task will append a .resources 
        /// extension to each filename in the Sources array and write each file to the directory 
        /// that contains the input file. If the user provides input (in the form of an array of 
        /// resources file names), the task will instruct resgen.exe to name the .resources
        /// files as provided by the user.
        /// </remarks>
        [Output]
        public ITaskItem[] OutputResources
        {
            set { outputResources = value; }
            get { return outputResources; }
        }

        /// <summary>
        /// The paths to references with types that may be used in the .resx files.
        /// </summary>
        /// <remarks>
        /// These references, along with resgen.exe, are copied to a temp subdirectory
        /// prior to invoking resgen.exe. This is a workaround because resgen.exe for 
        /// .NET 1.1 has no way of specifying references on the command line. 
        /// Once the resources have been generated, the copied references are deleted.
        ///
        /// Because ResolveAssemblyReference is invoked prior to GenerateResource, 
        /// the values in references should be fully resolved paths.
        /// </remarks>
        public ITaskItem[] References
        {
            set { references = value; }
            get { return references; }
        }

        #endregion // properties

        /// <summary>
        /// Make sure that OutputResources has 1 file name for each name in Sources.
        /// </summary>
        protected bool CreateOutputResourcesNames()
        {
            // If the user already provided names for the resources files, use them.
            if (outputResources != null)
            {
                if (outputResources.Length == sources.Length)
                {
                    return true;
                }
                else
                {
                    Log.LogMessageFromResources(MessageImportance.High, "DefaultOutputResources");
                }
            }

            // If the user didn't provide any names, create a new outputResources array and use the defaults.
            outputResources = new ITaskItem[sources.Length];

            int i = 0;
            try
            {
                for (i = 0; i < Sources.Length; ++i)
                {
                    outputResources[i] = new TaskItem(Path.ChangeExtension(sources[i].ItemSpec, ".resources"));
                }
            }
            catch (ArgumentException e)
            {
                Log.LogErrorWithCodeFromResources("InvalidFileName", sources[i].ItemSpec, e.Message);
                return false;
            }

            return true;
        }

        /// <summary>
        /// Creates a uniquely named temp subdirectory.
        /// </summary>
        /// <returns>Path to the uniquely named subdirectory on success; null on failure.</returns>
        protected string CreateUniqueTempDirectory()
        {
            string tempFile = "";
            string tempDir;

            try
            {
                // Create a uniquely named zero-byte file
                tempFile = Path.GetTempFileName();

                // Build a directory path using the uniquely named file.
                tempDir = Path.Combine(Path.GetDirectoryName(tempFile), Path.GetFileNameWithoutExtension(tempFile));
                tempDir += Path.DirectorySeparatorChar;

                // Create a directory under temp using the directory path.
                Directory.CreateDirectory(tempDir);

                // Delete the temp file.
                File.Delete(tempFile);
            }
            catch (System.IO.IOException ex)
            {
                Log.LogErrorFromResources("DirectoryCreationFailed", ex.Message, ex.StackTrace);
                return null;
            }

            // Return the name of the unique directory.
            return tempDir;
        }

        /// <summary>
        /// Copies specified references to the temp directory so resgen.exe will find them.
        /// </summary>
        /// <returns>True if copying is successful; false otherwise.</returns>
        protected bool CopyReferences()
        {
            string referenceShortName, destFileName;

            // If there are no references, then there's nothing to copy so just return true.
            if ((references == null) || (references.Length == 0))
            {
                return true;
            }

            foreach (ITaskItem reference in references)
            {
                referenceShortName = Path.GetFileName(reference.ItemSpec);
                destFileName = Path.Combine(tempResGenPath, referenceShortName);

                try
                {
                    Log.LogMessageFromResources(MessageImportance.Normal, "FileCopy", reference, destFileName);
                    File.Copy(reference.ItemSpec, destFileName, true);

                    // Set the copied file's attributes to Normal so the file is deleteable.
                    File.SetAttributes(destFileName, FileAttributes.Normal);
                }
                catch (System.IO.IOException ex)
                {
                    Log.LogErrorFromResources("FileCopyFailed", reference, destFileName, ex.Message, ex.StackTrace);
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// Copy ResGen.exe to the temp directory.
        /// </summary>
        /// <returns>True if copying is successful; false otherwise.</returns>
        protected bool CopyResGen()
        {
            string srcFileName = OriginalPathToTool();
            string destFileName = GenerateFullPathToTool();

            try
            {
                Log.LogMessageFromResources(MessageImportance.Normal, "FileCopy", srcFileName, destFileName);
                File.Copy(srcFileName, destFileName);
            }
            catch (System.IO.IOException ex)
            {
                Log.LogErrorFromResources("FileCopyFailed", srcFileName, destFileName, ex.Message, ex.StackTrace);
                return false;
            }

            return true;
        }

        /// <summary>
        /// Deletes the temporary ResGen directory and the files copied to it.
        /// </summary>
        /// <remarks>
        /// This method originally just invoked Directory.Delete() with recursive deletion set to true. 
        /// Occasionally, an UnauthorizedAccessException would be thrown when trying to delete resgen.exe
        /// in the temp directory. A workaround, that attempted deletion multiple times when access was
        /// denied, was tried but testing revealed it didn't always work. The technique now being
        /// used is to delete the files in the directory first. A message is displayed if an access is 
        /// denied error occurs. Once we attempt to delete all the files in the directory, when then try
        /// to delete the directory and any remaining files in it. If an access is denied error still occurs, 
        /// a build error is logged. Thus far, we've experience no build errors with this technique.
        /// </remarks>
        /// <returns>True if deletion is successful; false otherwise.</returns>
        protected bool DeleteTempResGenPath()
        {
            foreach (string tempFile in Directory.GetFiles(tempResGenPath))
            {
                try
                {
                    Log.LogMessageFromResources(MessageImportance.Normal, "Delete", tempFile);
                    File.Delete(tempFile);
                }
                catch (UnauthorizedAccessException)
                {
                    Log.LogMessageFromResources(MessageImportance.Normal, "DeleteFailedWillRetry", tempFile);
                }
            }

            try
            {
                Log.LogMessageFromResources(MessageImportance.Normal, "DeleteDirectory", tempResGenPath);
                Directory.Delete(tempResGenPath, true);
            }
            catch (UnauthorizedAccessException uaex)
            {
                Log.LogErrorFromResources("DeleteFailedWithException", tempResGenPath, uaex.Message, uaex.StackTrace);
                return false;
            }

            return true;
        }

        /// <summary>
        /// Invokes resgen.exe by building its command line and invoking ExecuteTool(). 
        /// </summary>
        /// <remarks>
        /// Before executing, any references are copied along with resgen.exe to a temp subdirectory. 
        /// After execution, the temp subdirectory and its contents are deleted.
        /// </remarks>
        /// <returns>
        /// True if resgen executes successfully and any reference copying and deletion succeeds; 
        /// false otherwise.
        /// </returns>
        private bool ExecuteResgen()
        {
            // Build the command line

            int retVal = -1;
            CommandLineBuilder commandLineBuilder = new CommandLineBuilder();

            // Append the compile switch
            commandLineBuilder.AppendSwitch("/compile");

            // Append the resources to compile
            for (int i = 0; i < sources.Length; i++)
            {
                commandLineBuilder.AppendFileNamesIfNotNull
                (
                new string[] { sources[i].ItemSpec, outputResources[i].ItemSpec },
                    ","
                );
            }

            tempResGenPath = CreateUniqueTempDirectory();

            // Create the temp directory and copy references and resgen.exe into it.
            if (String.IsNullOrEmpty(tempResGenPath) || !Directory.Exists(tempResGenPath))
            {
                return false;
            }

            if (!CopyReferences() || !CopyResGen())
            {
                // Clean up if either copy method fails and return false.
                DeleteTempResGenPath();
                return false;
            }

            // Log the full command line and execute resgen.exe in the temp directory. 
            Log.LogCommandLine(MessageImportance.High, GenerateFullPathToTool() + " " + commandLineBuilder.ToString());
            retVal = base.ExecuteTool(GenerateFullPathToTool(), null, commandLineBuilder.ToString());

            // Delete the temporary directory and its contents.
            DeleteTempResGenPath();

            // If Resgen.exe was successful, return true. Otherwise, return false.
            if (retVal == 0)
            {
                // Returns a failure if an error was logged after resgen.exe executed.
                return !Log.HasLoggedErrors;
            }

            return false;
        }

        public override bool Execute()
        {
            // If there are no sources to process, just return (with success) and report the condition.
            if ((sources == null) || (sources.Length == 0))
            {
                // Indicate we generated nothing
                Log.LogMessageFromResources(MessageImportance.Low, "NoSources");
                outputResources = null;
                return true;
            }

            // If Resgen.exe isn't present under SDK\bin, return false.
            if (!File.Exists(OriginalPathToTool()))
            {
                outputResources = null;
                return false;
            }

            // If creating the resource names failed, return false.
            if (!CreateOutputResourcesNames())
            {
                outputResources = null;
                return false;
            }

            for (int i = 0; i < sources.Length; i++)
            {
                // Attributes from input items should be forwarded to output items.
                sources[i].CopyMetadataTo(outputResources[i]);
            }

            // If there are resources out of date, call ExecuteResgen. If ExecuteResgen fails, return false;
            if (!ExecuteResgen())
            {
                outputResources = null;
                return false;
            }

            // Return !Log.HasLoggedErrors so if errors are logged in a method but false isn't returned,
            // we'll still return false here.
            return !Log.HasLoggedErrors;
        }
    }
}